package uk.co.richardwkirk.parkrun.funnels.model;

import java.util.LinkedList;

import uk.co.richardwkirk.parkrun.events.ParkrunDataLoadException;
import uk.co.richardwkirk.parkrun.web.ParkrunPageLoader;

public class RaceFinishers {

	LinkedList<Runner> remainingRunners = null;
	
	private RaceFinishers() {
		remainingRunners = new LinkedList<Runner>();
	}
		
	public static RaceFinishers parseCSV(String csvContent) throws RaceFinisherException
	{
		// Load the data from file
		String[] allLines = csvContent.split("\n");
		
		// Create the runners from the file input
		RaceFinishers runners = new RaceFinishers();
		for (String nextLine : allLines) {
			String[] allElements = nextLine.split(",");
			String name = allElements[1];
			int position = Integer.parseInt(allElements[0]);
			long finishTime = 0;
			if (!name.equals("Unknown")) {
				finishTime = parseFinishTime(allElements[2]);
			}
			Runner runner = new Runner(name, position, finishTime);
			runners.remainingRunners.add(runner);
		}
		return runners;
	}

	
	protected static Node getResultsTableNode(NodeList allNodes)
	{
		// Create a filter to extract the table node we are interested in
		NodeFilter elementFilter = new TagNameFilter("table");
		NodeFilter idAttribute = new HasAttributeFilter("id", "results");
		NodeFilter filter = new AndFilter(elementFilter, idAttribute);

		// Find all the nodes that match the filter
		NodeList foundNodes = allNodes.extractAllNodesThatMatch(filter);
		if (foundNodes.size() > 0) {
			// If we have found the table node, we return it.
			return foundNodes.elementAt(0);
		}
		else {
			// Otherwise we dig down into the child nodes from the original node list
			for (int i = 0; i < allNodes.size(); ++i) {
				Node nextNode = allNodes.elementAt(i);
				NodeList children = nextNode.getChildren();
				if (children != null) {
					Node tableNode = getResultsTableNode(nextNode.getChildren());
					if (tableNode != null) {
						return tableNode;
					}
				}
			}
		}
		return null;
	}
	
	
	public static RaceFinishers parseHTML(String htmlContent) throws RaceFinisherException
	{
		// We are creating a list of runners
		RaceFinishers runners = new RaceFinishers();
		
		// Create a parser over the HTML content
		Parser parser = Parser.createParser(htmlContent, "UTF-8");
		try {
			// Parse the document to get the node list 
			NodeList htmlList = parser.parse(null);
			
			// Get the table node that we are interested in
			Node tableNode = getResultsTableNode(htmlList);
			
			// Get the rows we are interested in
			NodeList runnerRows = getRunnerRowsFromTable(tableNode);
			if (runnerRows == null) {
				throw new RaceFinisherException("No runner result rows found from table.");
			}
			
			// Create a Runner from each row - ignoring the header row
			for (int i = 2; i < runnerRows.size(); ++i) {
				Node runnerRow = runnerRows.elementAt(i);
				Runner nextRunner = createRunnerFromTableRow(runnerRow);
				runners.remainingRunners.add(nextRunner);
			}
		}
		catch (ParserException e) {
			throw new RaceFinisherException("Cannot parse HTML from parkrun site.", e);
		}
		
		return runners;
	}

	
	private static Runner createRunnerFromTableRow(Node runnerRow) throws RaceFinisherException {
		try {
			// Get the elements we are interested in
			NodeList columns = runnerRow.getChildren().extractAllNodesThatMatch(new TagNameFilter("td"));
			
			Node positionNode = columns.elementAt(0);
			Node nameNode = columns.elementAt(1);
			Node finishTimeNode = columns.elementAt(2);
			
			Runner runner = null;
			String name = (nameNode.getFirstChild().getChildren() != null ? nameNode.getFirstChild().getFirstChild() : nameNode.getFirstChild()).getText();
			int position = Integer.parseInt(positionNode.getFirstChild().getText());
			Node finishTimeTextNode = finishTimeNode.getFirstChild();
			if (finishTimeTextNode != null) {
				long finishTime = parseMMSSFinishTime(finishTimeTextNode.getText());
				runner = new Runner(name, position, finishTime);
			}
			else {
				runner = new Runner(name, position, 0);
			}
			return runner;
		}
		catch (NullPointerException | NumberFormatException e) {
			throw new RaceFinisherException("Cannot build runner from table row.", e);
		}
	}

	private static long parseMMSSFinishTime(String text) throws RaceFinisherException {
		String[] parts = text.split(":");
		if (parts.length < 2) {
			throw new RaceFinisherException("Could not parse finish time: " + text);
		}
		int mins = Integer.parseInt(parts[0]);
		int secs = Integer.parseInt(parts[1]);
		return (mins * 60 + secs) * 1000; 
	}

	private static NodeList getRunnerRowsFromTable(Node tableNode) throws RaceFinisherException {
		// Get the table body element
		NodeList trElements = tableNode.getChildren().extractAllNodesThatMatch(new TagNameFilter("tr"));
		return trElements;
	}

	public static RaceFinishers createFromResource(String resourceAddress) throws RaceFinisherException
	{	
		// Get the contents we need to populate the race finishers list
		ParkrunPageLoader dataLoader = new ParkrunPageLoader();
		String parkrunData;
		try {
			parkrunData = dataLoader.load(resourceAddress);
		} catch (ParkrunDataLoadException e) {
			throw new RaceFinisherException("Could not load list of race finishers", e);
		}
		
		// Parse the contents of the input page to get the race finishers
		RaceFinishers finishers = RaceFinishers.parse(parkrunData);
		return finishers;
	}
	
	
	private static RaceFinishers parse(String finishersContent) throws RaceFinisherException {
		RaceFinishers runners = null;
		if (finishersContent.contains("html")) {
			runners = RaceFinishers.parseHTML(finishersContent);
		}
		else {
			runners = RaceFinishers.parseCSV(finishersContent);
		}
		runners.setUnknownTimes();
		return runners;
	}

	
	private void setUnknownTimes() {
		for (int i = 0; i < remainingRunners.size(); ++i)
		{
			Runner runner = remainingRunners.get(i);
			if (runner.getFinishTime() == 0.0)
			{
				Runner previousRunner = remainingRunners.get(i-1);
				long previousTime = previousRunner.getFinishTime();
				
				Runner nextRunner = runner;
				int nextRunnerId = i;
				while (nextRunner.getFinishTime() == 0) {
					++nextRunnerId;
					nextRunner = remainingRunners.get(nextRunnerId);
				}
				long nextTime = nextRunner.getFinishTime();
				
				if (previousTime == 0.0) {
					runner.setFinishTime(nextTime);
				}
				else if (nextTime == 0.0) {
					runner.setFinishTime(previousTime);
				}
				else {
					long averageTime = (nextTime + previousTime) / 2;
					runner.setFinishTime(averageTime);
				}
			}
		}
	}

	private static long parseFinishTime(String timeString) {
		String[] timeElements = timeString.split(":");
		long mins = Integer.parseInt(timeElements[0]) * 60000;
		long secs = Integer.parseInt(timeElements[1]) * 1000;
		return mins + secs;
	}

	public boolean allFinished() {
		return remainingRunners.isEmpty();
	}

	public LinkedList<Runner> removeFinishersBefore(long currentTime) {
		LinkedList<Runner> finishers = new LinkedList<Runner>();
		int removeCount = 0;
		for (Runner nextRunner : remainingRunners)
		{
			if (nextRunner.getFinishTime() <= currentTime)
			{
				finishers.add(nextRunner);
				++removeCount;
			}
			else
			{
				// Sorted by finish time so no need to go further
				break;
			}
		}
		for (int i = 0; i < removeCount; ++i)
		{
			remainingRunners.removeFirst();
		}
		return finishers;
	}


	public int size() {
		return remainingRunners.size();
	}

	public Runner getFinisher(int position) {
		for (Runner runner : remainingRunners)
		{
			if (runner.position == position)
			{
				return runner;
			}
		}
		return null;
	}

}
